#!/usr/bin/python2.4 -S
#
# Copyright 2007 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import distutils.command.bdist_dumb
import distutils.command.build
import distutils.command.build_py
import distutils.command.install_scripts
import distutils.log
import distutils.sysconfig
import getpass
import os
import stat
import re
import subprocess
import sys
import tempfile
import traceback

from distutils.core import setup
from errno import EEXIST


VERSION_MAJOR = 0
VERSION_MINOR = 1
VERSION = '%d.%d' % (VERSION_MAJOR, VERSION_MINOR)


SVN = 'svn'
SVNVERSION = 'svnversion'


def GetVersion():
  """Return version string for this gvn build.

  If svn and svnversion are on PATH and this gvn directory is a
  working copy of gvn trunk, return 'rNNN' where 'NNN' is the output
  of svnversion.  Else, return VERSION.
  """

  try:
    info = subprocess.Popen([SVN, 'info'],
                            stdout=subprocess.PIPE).communicate()[0]
  except OSError:
    info = None

  if info is not None and info != '':
    for line in info.splitlines():
      (key, val) = line.split(':', 1)
      if key == 'URL':
        release = '//gvn.googlecode.com/svn/release/' in val
        break
    if not release:
      try:
        svnversion = subprocess.Popen([SVNVERSION],
                                      stdout=subprocess.PIPE).communicate()[0]
      except OSError:
        svnversion = None
      if svnversion is not None:
        svnversion = svnversion.strip().replace(':', '-')
        return 'r' + svnversion

  return VERSION


def rename(src, dst):
  try:
    os.rename(src, dst)
  except OSError, e:
    if sys.platform != 'win32' or e.errno != EEXIST:
      raise
    # "On Windows, if dst already exists, OSError will be raised even if it
    # is a file; there may be no way to implement an atomic rename when dst
    # names an existing file."
    os.remove(dst)
    os.rename(src, dst)


###########################################################################
# svn subcommand and option scanning

def SvnSubcommands():
  """Yield (name, list of aliases) for each subcommand."""
  pipe = subprocess.Popen([SVN, '-h'], stdout=subprocess.PIPE).stdout
  for line in pipe:
    if line == 'Available subcommands:' + os.linesep:
      # Subcommands follow; here we go!
      break
  for line in pipe:
    # Line is of one of these three forms:
    #   delete (del, remove, rm)
    #   diff (di)
    #   export
    # match group 1 is 'delete', 'diff', or 'export'
    # match group 3 is '(del, remove, rm)', '(di')', or None
    m = re.match(r'   ([A-Za-z]+)( \(((([A-Za-z]+)(, )?)+)\))?', line)
    if m is None:
      # No more subcommands; we're outta here.
      break
    aliases = m.group(3)
    if aliases is None:
      aliases = ()
    else:
      aliases = aliases.split(', ')
    yield (m.group(1), aliases)

def SvnSubcommandOptions(subcommand):
  """Return {long: (short, has_argument)} for subcommand options.

  Returns a dict so callers can avoid duplicates.
  """
  options = {}
  pipe = subprocess.Popen([SVN, subcommand, '-h'],
                          stdout=subprocess.PIPE).stdout
  for line in pipe:
    if line == 'Valid options:' + os.linesep:
      # Options follow; here we go!
      break
  for line in pipe:
    if line == os.linesep:
      # No more options; we're outta here.
      break
    if line.startswith('   '):
      # Skip help continuation lines.
      continue
    m = re.match(r'  -([A-Za-z]|-(\S+))(\s+\[--(\S+)\])?(\s+ARG)?', line)
    if m is None:
      raise distutils.errors.DistutilsExecError('parsing svn %s -h: %s'
                                                % (subcommand, line.rstrip()))
    has_argument = m.group(5) is not None
    if m.group(2) is None:
      # long + short
      # (1)  ( 4  )( 5 )
      # -F [--file] ARG          : read lock comment from file ARG
      # -N [--non-recursive]     : obsolete; try --depth=files ...
      # match group 1 is '-F' or '-N'
      # match group 4 is '--file' or '--non-recursive'
      # match group 5 is 'ARG' or None
      long = m.group(4)
      short = "'" + m.group(1) + "'"
    else:
      # just long
      #  (  2   )( 5 )
      # --accept  ARG            : specify automatic conflict ...
      # --auto-props             : enable automatic properties
      # match group 2 is '--accept' or '--auto-props'
      # match group 5 is 'ARG' or None
      long = m.group(2)
      short = None
    options[long] = (long, short, has_argument)
  return options


###########################################################################
# command classes

class build(distutils.command.build.build):
  """Subclass of build subcommand for optional hard-wiring of paths to
  svn binary and Subversion Python bindings.
  """
  user_options = distutils.command.build.build.user_options + [
      ('with-svn-bindir=', None,
       'specify directory containing svn binaries'),
      ('with-svn-pydir=', None,
       'specify Python libdir containing svn modules'),
      ]

  def initialize_options(self, *args, **kwargs):
    self.with_svn_bindir = self.with_svn_pydir = None
    distutils.command.build.build.initialize_options(self, *args, **kwargs)

  def run(self, *args, **kwargs):
    # XXX gotta be a better way than globals
    global SVN_BINDIR, SVN_PYDIR

    SVN_BINDIR = self.with_svn_bindir
    SVN_PYDIR = self.with_svn_pydir

    if SVN_BINDIR is not None:
      global SVN, SVNVERSION
      SVN = os.path.join(SVN_BINDIR, 'svn')
      SVNVERSION = os.path.join(SVN_BINDIR, 'svnversion')

    distutils.command.build.build.run(self, *args, **kwargs)


class build_py(distutils.command.build_py.build_py):
  """Subclass of build_py subcommand for creating gvn/__init__.py and
  gvn/subcommands/__init__.py .
  """

  def build_packages(self, *args, **kwargs):
    global SVN_BINDIR, SVN_PYDIR

    # XXX Good grief.  'setup.py install' calls this again, without
    # the --with-svn-* options, clobbering gvn/__init__.py.
    if sys.argv[1] != 'build':
      return

    distutils.command.build_py.build_py.build_packages(self, *args, **kwargs)

    # TODO(epg): Plug VERSION stuff in somewhere, in gvn/__init__.py
    # or a gvn/version.py or something.

    # Construct a __init__.py for gvn that puts the svn libraries on
    # sys.path and provides paths to svn binaries.
    outfile = self.get_module_outfile(self.build_lib, ['gvn'], '__init__')
    (fd, fn) = tempfile.mkstemp(dir=os.path.dirname(outfile))
    fp = os.fdopen(fd, 'w')

    # Write the version info.
    fp.write('VERSION = "%s"\n' % (GetVersion(),))

    # If we have no SVN_BINDIR, simply call the executables, trusting
    # that they're on the user's path, rather than leaving them
    # undefined.
    if SVN_BINDIR is None:
      fp.write("SVN = 'svn'\n"
               "SVNADMIN = 'svnadmin'\n")
    else:
      fp.write("SVN = '%s'\n"
               "SVNADMIN = '%s'\n" % (os.path.join(SVN_BINDIR, 'svn'),
                                      os.path.join(SVN_BINDIR, 'svnadmin')))

    if SVN_PYDIR is not None:
      fp.write("import sys\n"
               "sys.path.insert(0, '%s')\n"
               % (SVN_PYDIR,))

    # Copy existing contents.
    fp.write(open('lib/gvn/__init__.py').read())

    fp.close()
    rename(fn, outfile)

    # Construct a __init__.py for gvn.subcommands that loads all our
    # subcommands, then bin/gvn can just do 'import gvn.subcommands'.
    subcommands = []
    for i in os.listdir(os.path.join(self.get_package_dir('gvn'),
                                     'subcommands')):
      if not i.endswith('.py') or i.endswith('_test.py'):
        continue
      subcommands.append(i[:-3])

    outfile = self.get_module_outfile(self.build_lib,
                                      ['gvn', 'subcommands'],
                                      '__init__')
    (fd, fn) = tempfile.mkstemp(dir=os.path.dirname(outfile))
    fp = os.fdopen(fd, 'w')
    fp.write('from gvn.subcommands import ')
    fp.write(', '.join(subcommands))
    fp.write('\n')
    fp.close()
    rename(fn, outfile)

    # Build gvn.svncmdline to contain the dict of all svn options.
    outfile = self.get_module_outfile(self.build_lib, ['gvn'], 'svncmdline')
    # TODO: only if outfile older than SVN:
    subcommands = list(SvnSubcommands())
    (fd, fn) = tempfile.mkstemp(dir=os.path.dirname(outfile))
    fp = os.fdopen(fd, 'w')
    fp.write('Subcommands = {\n')
    for (subcommand, aliases) in subcommands:
      fp.write("  '%s': [%s],\n" % (subcommand,
                                    ', '.join("'%s'" % (a,) for a in aliases)))
    fp.write('}\n')
    fp.write('Options = {\n')
    options_written = set()
    for (subcommand, aliases) in subcommands:
      # TODO(epg): write out subcommand and aliases
      for (long_opt, opt_tuple) in SvnSubcommandOptions(subcommand).iteritems():
        if long_opt in options_written:
          continue
        fp.write("  '%s': (%s, %s, None),\n" % opt_tuple)
        options_written.add(long_opt)
    fp.write('}\n')
    fp.close()
    rename(fn, outfile)


# Exact copy of install_scripts from Python 2.4.4, with a little Mac hack.
class install_scripts (distutils.command.install_scripts.install_scripts):
    def run (self):
        if not self.skip_build:
            self.run_command('build_scripts')
        self.outfiles = self.copy_tree(self.build_dir, self.install_dir)
        if os.name == 'posix':
            # Set the executable bits (owner, group, and world) on
            # all the scripts we just installed.
            for file in self.get_outputs():
                if self.dry_run:
                    distutils.log.info("changing mode of %s", file)
                else:
                    mode = ((os.stat(file).st_mode) | 0555) & 07777
                    distutils.log.info("changing mode of %s to %o", file, mode)
                    os.chmod(file, mode)

        ##############################################################
        # Mac hack: At least the python.org installer has distutils
        # installing scripts under /Library/... where they won't be on
        # the user's PATH; not sure if the Apple-shipped Python is the
        # same way.  But, the python.org installer symlinks the python
        # binary to /usr/local/bin, so let's do the same.
        (bindir,) = distutils.sysconfig.get_config_vars('BINDIR')
        if bindir is not None and bindir.startswith('/Library'):
          dst_dir = self.install_dir[:-len(bindir)]  + '/usr/local/bin'
          self.mkpath(dst_dir)
          src = '../../..' + bindir + '/gvn'
          dst = dst_dir + '/gvn'
          distutils.log.info('symlink %s -> %s', src, dst)
          try:
            os.symlink(src, dst)
          except OSError, e:
            if e.errno != EEXIST:
              raise
        ##############################################################


# Exact copy of bdist_dumb from Python 2.4.4, with mode and ownership
# hacks, since distutils is stupid enough to install files owned by
# the building user and ONLY READABLE by the building user.  It's
# easier to fix this for bdist_dumb than for install generally because
# this way we can just walk the temporary install tree.
class bdist_dumb (distutils.command.bdist_dumb.bdist_dumb):
    def run (self):
        from distutils import log
        from distutils.dir_util import create_tree, remove_tree, ensure_relative

        if not self.skip_build:
            self.run_command('build')

        install = self.reinitialize_command('install', reinit_subcommands=1)
        install.root = self.bdist_dir
        install.skip_build = self.skip_build
        install.warn_dir = 0

        log.info("installing to %s" % self.bdist_dir)
        self.run_command('install')

        ##############################################################
        # HACK(epg): chmod and chown
        for root, dirs, files in os.walk(self.bdist_dir):
          for fn in dirs + files:
            path = os.path.join(root, fn)
            if os.geteuid() == 0:
              # chown to root:root if possible.
              os.lchown(path, 0, 0)
            # Make sure everyone can read or execute.
            mode = stat.S_IMODE(os.stat(path).st_mode)
            if mode & stat.S_IRUSR:
              mode |= stat.S_IRGRP | stat.S_IROTH
            if mode & stat.S_IXUSR:
              mode |= stat.S_IXGRP | stat.S_IXOTH
            os.chmod(path, mode)
        if os.geteuid() == 0:
          # chown to root:root if possible.
          os.chown(self.bdist_dir, 0, 0)
        ##############################################################

        # And make an archive relative to the root of the
        # pseudo-installation tree.
        archive_basename = "%s.%s" % (self.distribution.get_fullname(),
                                      self.plat_name)

        # OS/2 objects to any ":" characters in a filename (such as when
        # a timestamp is used in a version) so change them to hyphens.
        if os.name == "os2":
            archive_basename = archive_basename.replace(":", "-")

        pseudoinstall_root = os.path.join(self.dist_dir, archive_basename)
        if not self.relative:
            archive_root = self.bdist_dir
        else:
            if (self.distribution.has_ext_modules() and
                (install.install_base != install.install_platbase)):
                raise DistutilsPlatformError, \
                      ("can't make a dumb built distribution where "
                       "base and platbase are different (%s, %s)"
                       % (repr(install.install_base),
                          repr(install.install_platbase)))
            else:
                archive_root = os.path.join(self.bdist_dir,
                                   ensure_relative(install.install_base))

        # Make the archive
        self.make_archive(pseudoinstall_root,
                          self.format, root_dir=archive_root)

        if not self.keep_temp:
            remove_tree(self.bdist_dir, dry_run=self.dry_run)


# Include upload command if possible.
try:
  from googlecode_distutils_upload import upload
except ImportError:
  class upload(distutils.core.Command):
    user_options = []
    def __init__(self, *args, **kwargs):
      raise distutils.errors.DistutilsModuleError("""\
Install these module in site-packages to upload:
 http://support.googlecode.com/svn/trunk/scripts/googlecode_upload.py
 http://support.googlecode.com/svn/trunk/scripts/googlecode_distutils_upload.py
""")


# TODO: Don't call GetVersion here, before we've set SVN as specified
# on the comand line.  I suppose we can override the version later...
setup(name='gvn', version=GetVersion(),
      license='Apache 2.0',

      url='http://code.google.com/p/gvn/',
      author_email='gvn-discuss@googlecode.com',

      package_dir={'': 'lib'},
      packages=['gvn', 'gvn/subcommands', 'gvn/hooks', 'gvn/hooks/pre_commit',
                'gvn/hooks/pre_revprop_change', 'gvn/hooks/post_commit' ],
      scripts=['bin/gvn', 'bin/gvn-hook-runner'],

      cmdclass={
                'bdist_dumb': bdist_dumb,
                'build': build,
                'build_py': build_py,
                'install_scripts': install_scripts,
                'upload': upload,
                },
      )
